﻿/****************************************************************
 * Содержит XML документ, используемый для трансформации
 * Документ может быть явно назначен, подгружен по URL или
 * из XML строки
 * 
 * Паблик методы:
 * - конструктор - создаёт пустой экземпляр, инициализирует пустые же значения внутренних
 *   переменных
 * - loadXml - загружает XML информацию в зависимости от указанного источника
 *   - xmlString, xmlDoc или url
 * - loadFromString - производит загрузку XML документа из XML строки. Используется как внутри
 *    метода loadXml, так и снаружи, если требуется преобразовать XML строку в документ.
 * - loadFromUrl - загружает XML документ по ссылке. Адрес ссылки должен заранее быть указан в переменной
 *   url. Используется в методе loadXml
 *
 * Исключения
 * в случае обнаружения известной ошибки, поднимаются исключения в виде объекта с двумя полями
 * - number - код ошибки.
 * - message - поясняющий текст
 * - original - оригинальное исключение/ошибка, если присутствует
 *
 * Список исключений (для всех методов)
 * Код | Где случается          | Описание
 * 101   loadFromUrl              Извините, ваш браузер не может работать с XML документами
 * 102   loadFromUrl              Не удалось загрузить файл XML
 * 103   loadFromString           Не удалось преобразовать строку в XML документ
 * 104   makeRequest              Невозможно создать XMLHTTP экземпляр
 * 105   fixIncludes              Не удалось исправить ссылки для конструкций xsl:include и xsl:import (версия для IE)
 * 106   fixIncludes              Не удалось исправить ссылки для конструкций xsl:include и xsl:import (версия для !IE)
 * 107   safariimportincludefix   Ошибка при получении include файла
 * 108   addParams                Не удалось заменить значения xsl:param в трансформации
 * 109   loadXml                  Документ не может быть загружен - не указано ни одного источника
 ****************************************************************/
function transformationDocument() {
    this.xmlDoc = null;
    this.url = null;
    this.xmlString = null;
    //this.browser = {};
    //this.detectBrowser();
    this.xslParams = null;
}

// метод загружает XML, автоматически определяя, из какого источника должна быть взята информация
// исключения:
// 109 - Документ не может быть загружен - не указано ни одного источника
transformationDocument.prototype.loadXml = function () {
    if (this.xmlDoc != null) {
        // если xmlDoc непуст, значит значение документа было установлено явно и подгружать его не нужно
        return;
    }
    if (this.xmlString != null) {
        // пытаемся подгрузить XML Document из XML String
        this.loadFromString(this.xmlString);
    }
    if (this.url != null) {
        // документ должен быть загружен по ссылке
        this.loadFromUrl(this.url);
        return;
    }
	
	throw {number: 109, message: "Документ не может быть загружен - не указано ни одного источника"}
}
// метод производит загрузку XML Document из строки
// исключения:
// 103 - не удалось преобразовать строку в XML Document
transformationDocument.prototype.loadFromString = function (xmlString) {
	try
		{
		if (window.ActiveXObject) { // IE
			this.xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
			this.xmlDoc.loadXML(xmlString);
		} else {
			var parser = new DOMParser();
			this.xmlDoc = parser.parseFromString(xmlString, "text/xml");
		};
	} catch (ex) {
		throw {number: 103, message: "Не удалось преобразовать строку в XML документ", original: ex };
	}
}

// метод осуществляет загрузку XML документа по указанному url
// ВНИМАНИЕ: сервер должен отдавать корректный заголовок файла
// иначе система не распознает, что это XML и соответственно responseXML будет пуст
// параметры
// {String} url - адрес, с которого требуется "забрать" документ
// ошибки:
// 101 - браузер не работает с XML документами
// 102 - не удалось загрузить файл XML
transformationDocument.prototype.loadFromUrl = function (url) {
    this.xmlDoc = null;

    if (window.ActiveXObject) { // IE
        this.xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
    } else if (document.implementation
            && document.implementation.createDocument) { // Mozilla, Firefox, Opera, etc.
        this.xmlDoc = document.implementation.createDocument("", "", null);
        if (!this.xmlDoc.load) { // Safari lacks on this method,
            // so we make a synchronous XMLHttpRequest
            var request = this.makeRequest(url, null, null, true);
            if (request.responseXML) {
                // подгружен реальный XML Document
                this.xmlDoc = request.responseXML;
            }
            else {
                // XML Document не найден. Очевидно, на сервере неверно выставлен
                // MIME-тип, попытаемся самостоятельно распарсить данные как будто это
                // XML String
                this.loadFromString(request.responseText);
            }
            return;
        }
    } else {
        throw {number: 101, message: "Извините, ваш браузер не может работать с XML документами"};
    }
    this.xmlDoc.async = false;
    try {
        this.xmlDoc.load(url);
    } catch (e) {
        throw {number: 102, message: "Не удалось загрузить файл XML"};
    }
    //return (this.xmlDoc);
}


// метод осуществляет обращение по указанному в locationURL адресу
// список параметров и возвращаемые значения
// {String} locationURL адрес, по которому будет производиться обращение. Без параметров
// {String} [parameters=null] список параметров ссылки в формате
//          'param1=value1&m2=value2&m3=value3'
// {Function(XHMLHTTPRequest, Object)} [onComplete=null] функция, которая должна быть вызвана когда
//        получен окончательный ответ от сервера (responseText или responseXML)
// {Boolean} [doSynchronous=false] флаг, указывающий, должен ли запрос быть синхронным 
//        (будет ли вызываться метод onComplete)
// {Boolean} [doPost=false] флаг, указывающий, что должен выполняться запрос POST вместо GET
// {Object} [dataPackage=null] любой объект, который будет передаваться функции onComplete
//
// возвращает
// {XHMLHTTPRequest} объект HTTPRequest, если не случилось никаких исключений/ошибок
//
// исключения:
// 104 -  'Невозможно создать XMLHTTP экземпляр'
transformationDocument.prototype.makeRequest = function (locationURL, parameters, onComplete, doSynchronous, doPost, dataPackage) {
    var http_request = false;
    // пытаемся создать метод HTTPRequest для разных браузеров. Если обнаружена ошибка,
    // делаем попытку другим методом
    try {
        http_request = new ActiveXObject("Msxml2.XMLHTTP");
    } catch (e1) {
        try {
            http_request = new ActiveXObject("Microsoft.XMLHTTP");
        } catch (e2) {
            http_request = new XMLHttpRequest();
        }
    }
    if (!http_request) {
        // ни один из известных способов не сработал - очевидно, браузер вообще не поддерживает этот объект
        throw {number: 104, message: 'Невозможно создать XMLHTTP экземпляр'};
    }

    if (onComplete && !doSynchronous) {
        // запрос выполняется асинхронно и имеется обработчик события для onComplete
        completeListener = function () {
            if (http_request.readyState == 4) {
                if (http_request.status == 200) {
                    // ответ пришёл, передаём объект request и данные, прежде
                    // присланные в наш метод
                    onComplete(http_request, dataPackage)
                }
            }
        };
        http_request.onreadystatechange = completeListener;
    }

    if (doPost) {
        // стоит флаг - использовать метод POST
        // устанавливаем заголовки и передаём параметры
        http_request.open('POST', locationURL, !doSynchronous);
        http_request.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
        http_request.setRequestHeader("Content-length", parameters.length);
        http_request.setRequestHeader("Connection", "close");
        http_request.send(parameters);
    } else {
        // исользуем GET - требуется дополнить адрес, к которому мы обращаемся, списком параметров
        http_request.open('GET', locationURL + (parameters ? ("?" + parameters) : ""), !doSynchronous);
        http_request.send(null);
    }

    return http_request;
}

// метод исправляет проблемы с include/import и params
// целиком взят из библиотеки jquery.transform http://plugins.jquery.com/project/Transform
// ввиду того, что частичное использование этого функционала не предвидится,
// не стал разбивать его на методы, а оставил как есть
// исключения:
// 105 - Не удалось исправить ссылки для конструкций xsl:include и xsl:import (версия для IE)
// 106 - Не удалось исправить ссылки для конструкций xsl:include и xsl:import (версия для !IE)
transformationDocument.prototype.fixIncludes = function () {

	// функция модифицирует сокращённую ссылку, пытаясь достроить её до полного URI
    // параметры
    // {String} val - ссылка, которую необходимо исправить
    // {classTransformationDocument} o - экземпляр, отвечающий за работу с XSL трансформацией
    var replaceref = function (val, o) {
        o.url = o.url ? o.url : "";
        var c = location.protocol == "file:" && $.browser.msie ? "\\" : "/";
        // полный хост
		var path1 = location.protocol + c + c + location.host; 
        // имя каталога, в котором находится текущая обрабатываемая трансформация
		var path2 = location.pathname.substring(0, location.pathname.lastIndexOf(c) + 1) + o.url.substring(0, o.url.lastIndexOf("/") + 1); 

        var res = '';
        if (val.substring(0, 1) == c) {
            res = path1 + val;
        }
        else if (val.substring(0, 2) == "..") {
            var count = 0;
            while (val.indexOf("..") != -1) {
                val = val.substring(val.indexOf("..") + 3);
                count += 1;
            }

            var p = path1 + path2.substring(0, path2.length - 1);

            for (var x = 0; x < count; x++) {
                p = p.substring(0, p.lastIndexOf(c));
            }
            res = p + c + val;
        }
        /* Begin RA */
		// добавления от http://ra-project.net/xsl_tests/
        else if (val.substring(0, 7) == "http://" || val.substring(0, 8) == "https://") {
            res = val;
        }
        else if (val.substring(0, 2) == "./") {
            res = path1 + o.url.substring(0, o.c.xslPath.lastIndexOf("/") + 1) + val.substring(2, val.length);
        }
        /* End RA */
        else {
            res = path1 + path2 + val;
        }
        // вопрос кэширования нас пока не волнует
        /*
        if (!o.c.cacheNetXsl) {
        res += '?_' + new Date().getTime().toString();
        }
        */
        return res;
    };

    var fixref;
    if ($.browser.msie) {
		// если браузер IE, требуется только правильно указать значения href для xsl:import и xsl:include
        try {
            fixref = function (ref, xObj) {
                var vals = xObj.selectNodes(ref);
                for (var x = 0; x < vals.length; x++) {
                    vals[x].setAttribute("href", this.replaceref(vals[x].getAttribute("href"), this));
                }
            };

            fixref("//xsl:include", this.xmlDoc);
            fixref("//xsl:import", this.xmlDoc);

        } catch (ex) {
            // не удалось внести исправления
			throw {number: 105, message: "Не удалось исправить ссылки для конструкций xsl:include и xsl:import", original: ex};
        }
    } else {
        try {
			// функция возвращает массив со всеми непосредственными дочерними нодами
			// параметры
			// {XmlNode} obj - родительская нода, внутри которой ищем "детей"
			// {String} find - имя дочерних нод, которые требуется найти
            var childNodes = function (obj, find) {
                var cns = [];
                var objs = obj.getElementsByTagName(find);
                for (var x = 0; x < objs.length; x++) {
                    if (objs[x].parentNode == obj) {
						// учитываем только те дочерние ноды, которые являются непосредственнвми потомками
                        cns[cns.length] = objs[x];
                    }
                }

                return cns;
            };

			// исправления для Сафари (Webkit)
			// вместо import/include используется реальное содержимое файлов. Для этого они последовательно выкачиваются
			// посредством Ajax, разбираются входящие в них "param" и "template", а затем подставляются в оригинальный
			// документ
			// метод также рекурсивно исследует импортированные трансформации на import/include
			// исключения:
			// 107 - 'Ошибка при получении include файла'
            var safariimportincludefix = function (xObj, rootConfig) {
                // собираем все имеющиеся import/include в один общий массив
                var vals = $.merge($.makeArray(xObj.getElementsByTagName("import")), $.makeArray(xObj.getElementsByTagName("include")));

                for (var x = 0; x < vals.length; x++) {
                    var node = vals[x];
                    $.ajax({
                        passData: { node: node, xObj: xObj, rootConfig: rootConfig },
                        dataType: "xml",
                        cache: false,
                        async: false,
                        url: replaceref(node.getAttribute("href"), rootConfig),
                        success: function (xhr) {
                            try {
                                var _ = this.passData;
                                xhr = safariimportincludefix(xhr, _.rootConfig);

                                var imports = $.merge(
                                    childNodes(xhr.getElementsByTagName("stylesheet")[0], "param"),
                                    childNodes(xhr.getElementsByTagName("stylesheet")[0], "template")
                                );

                                var existingNodes = $.merge(
                                    childNodes(_.xObj.getElementsByTagName("stylesheet")[0], "param"),
                                    childNodes(_.xObj.getElementsByTagName("stylesheet")[0], "template")
                                );
                                var existingNames = [];
                                var existingMatches = [];
                                for (var a = 0; a < existingNodes.length; a++) {
                                    if (existingNodes[a].getAttribute("name")) {
                                        existingNames[existingNodes[a].getAttribute("name")] = true;
                                    } else {
                                        existingMatches[existingNodes[a].getAttribute("match")] = true;
                                    }
                                }

                                var pn = _.node.parentNode;
                                for (var y = 0; y < imports.length; y++) {
                                    if (!existingNames[imports[y].getAttribute("name")] && !existingMatches[imports[y].getAttribute("match")]) {
                                        var clonednode = _.xObj.importNode(imports[y], true);
                                        pn.insertBefore(clonednode, _.xObj.getElementsByTagName("template")[0]);
                                    }
                                }
                                pn.removeChild(_.node);
                            } catch (ex) {

                            }
                        },
                        error: function (xhr, status, error) {
                            throw {number: 107, message: 'Ошибка при получении include файла', original: error};
                        }
                    });
                }

                return xObj;
            };

            fixref = function (ref, xObj, o) {
                ref = $.browser.mozilla && $.browser.version.substring(0, 3) == "1.9" ? "xsl:" + ref : ref;
                var vals = xObj.getElementsByTagName(ref);
                for (var x = 0; x < vals.length; x++) {
                    vals[x].setAttribute("href", replaceref(vals[x].getAttribute("href"), o));
                }
            };

            if ($.browser.safari) {
                this.xmlDoc = safariimportincludefix(this.xmlDoc, this);
            } else {
                fixref("import", this.xmlDoc, this);
                fixref("include", this.xmlDoc, this);
            }
        } catch (ex) {
			if(ex.number && ex.number == 107) {
				throw ex;
			} else {
				throw {number: 106, message: "Не удалось исправить ссылки для конструкций xsl:include и xsl:import", original: ex};
			}
        }
    }
}

// метод производит явную подстановку значений "params"
// вызывается только для IE, так как "нативный" метод xsl:param там не работает
// исключения:
// 108 - Не удалось заменить значения xsl:param в трансформации
transformationDocument.prototype.addParams = function () {
    if ($.browser.msie) {
        for (var p in this.xslParams) {
            var strParam = "//xsl:param[@name='" + p + "']";

            try {
                var v = this.xslParams[p];
                if (typeof v === "boolean") {
                    v = "'" + v + "'";
                } else {
                    if (isNaN(parseInt(v)) && v.indexOf("'") < 0) {
                        v = "'" + v + "'";
                    }
                }
                var xslParam = this.xmlDoc.selectSingleNode(strParam);
                if (xslParam == null) {
                    var newparam = this.xmlDoc.createElement("xsl:param");
                    newparam.setAttribute("name", p);
                    this.xmlDoc.documentElement.insertBefore(newparam, this.xmlDoc.selectSingleNode("//xsl:template"));
                    xslParam = this.xmlDoc.selectSingleNode(strParam);
                }
                xslParam.setAttribute("select", v);
            } catch (ex) {
                throw {number: 108, message: "Не удалось заменить значения xsl:param в трансформации", original: ex}
            }
        }
    }
}
